Skip to content

fix: dependency installation gaps in Plugin Store and first-time install#385

Merged
ChuckBuilds merged 5 commits into
mainfrom
claude/plugin-dependency-auto-install-lknpd8
Jul 8, 2026
Merged

fix: dependency installation gaps in Plugin Store and first-time install#385
ChuckBuilds merged 5 commits into
mainfrom
claude/plugin-dependency-auto-install-lknpd8

Conversation

@ChuckBuilds

@ChuckBuilds ChuckBuilds commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Summary

Two related dependency-installation bugs, both root-caused from real user reports:

1. Plugin Store install/update path installed dependencies with a bare pip3

store_manager.py's install_plugin/update_plugin (the actual Plugin Store install/update path) installed requirements.txt with a bare pip3 off PATH, completely bypassing the root-visible installer added in #380 for the separate "Reinstall Plugin Deps" tool button. Two bugs stacked: (1) pip3 can resolve to a different Python installation than the one that actually runs ledmatrix.service, and (2) ledmatrix-web.service runs as a non-root user, so even a "successful" pip3 install lands in that user's local site-packages, invisible to root-run ledmatrix.service. Either way, the install reports success and writes the .dependencies_installed hash marker, so plugin_loader's own (correct) install-on-load path skips reinstalling — leaving the dependency permanently missing until someone finds and clicks the unrelated "Reinstall Plugin Deps" tool. This is why users kept hitting No module named 'astral' for the weather plugin even after installing it from the Store.

Extracted the sudo-wrapper-then-fallback install logic from api_v3.py's _pip_install_requirements into src/common/permission_utils.py as install_requirements_file, and routed both api_v3.py and store_manager.py through it, so the automatic Store install/update path now matches the manual "Reinstall Plugin Deps" path.

2. First-time install script fails on apt-managed requests

web_interface/requirements.txt and requirements.txt both pin requests>=2.33.0,<3.0.0, but Raspberry Pi OS ships an apt-managed python3-requests with no pip RECORD file. Upgrading it via a plain pip install aborts with uninstall-no-record-file, producing exactly the "Some web interface dependencies failed to install" warning reported during first-time install. scripts/install_dependencies_apt.py and scripts/fix_perms/safe_pip_install.sh already work around this with --ignore-installed; first_time_install.sh's own direct pip invocations (the per-package requirements.txt loop, the web_interface/requirements.txt install, and the requirements_web_v2.txt fallback) didn't. Added --ignore-installed to all three.

Type of change

  • Bug fix

Related issues

Related to #380 (fixes the automatic Plugin Store install/update path that #380 didn't cover).

Test plan

  • Ran the test suite (pytest test/test_plugin_loader.py test/test_store_manager_caches.py — 49 passed)
  • Ran the broader web API / state-reconciliation suites to confirm no regressions (pre-existing unrelated failures verified identical on main)
  • bash -n first_time_install.sh (syntax check)
  • Ran on a real Raspberry Pi with hardware

Documentation

  • N/A — no docs needed

Plugin compatibility

  • No plugin breakage expected

Checklist

  • I've not committed any secrets or hardcoded API keys

Notes for reviewer

Existing installs whose .dependencies_installed marker was already written by the old (broken) pip3 path won't be retroactively fixed by the Plugin Store change alone, since the marker's hash still matches an unchanged requirements.txt. Users currently affected can use the existing "Reinstall Plugin Deps" tool (Tools page) once, or reinstall the plugin, to pick up the corrected path.

Codacy flagged the new subprocess.run calls in install_requirements_file as a "critical" security issue (generic Bandit B603 pattern match on non-literal argv). Both calls use list-form argv with no shell=True, and the only dynamic value is an internally-constructed Path, never raw external input — the same pattern already used unmodified elsewhere in this codebase. Suppressed with inline # nosec B603 comments matching this repo's existing convention.

Summary by CodeRabbit

  • Bug Fixes
    • Improved dependency installation reliability during first-time setup and web interface installs.
    • Reduced failures when system-managed Python packages overlap with requested versions.
    • Made install error handling more consistent, with clearer fallback behavior when elevated install paths aren’t available.
    • Updated timeout guidance so the suggested manual install command matches the new installation behavior.

…ugin Store

install_plugin/update_plugin (store_manager.py) installed requirements.txt
with a bare `pip3` off PATH, bypassing the root-visible installer added in
#380 for the "Reinstall Plugin Deps" button. Two bugs stacked: (1) `pip3`
can resolve to a different Python install than the one that actually runs
ledmatrix.service, and (2) even when it resolves correctly, ledmatrix-web
runs as a non-root user so the package lands in that user's local
site-packages, invisible to root-run ledmatrix.service. Either way the
install reports success and writes the .dependencies_installed hash marker,
so plugin_loader's own (correct) install-on-load path skips reinstalling —
leaving the dependency permanently missing until a user finds and clicks
the separate "Reinstall Plugin Deps" tool. This is why users kept hitting
"No module named 'astral'" for the weather plugin even after installing it
from the Store.

Extracts the sudo-wrapper-then-fallback install logic from api_v3.py's
_pip_install_requirements into src/common/permission_utils.py as
install_requirements_file, and routes store_manager.py's dependency
installation through it so the automatic Store install/update path now
matches the manual "Reinstall Plugin Deps" path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X1NnDduw53kTe67i5zWwYx
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ChuckBuilds, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 54 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8bcd3b91-96b2-48d4-9857-9262c5fda11a

📥 Commits

Reviewing files that changed from the base of the PR and between 6b0c7bd and 630a733.

📒 Files selected for processing (1)
  • src/common/permission_utils.py
📝 Walkthrough

Walkthrough

Adds --ignore-installed to pip install commands in first_time_install.sh to avoid apt/pip RECORD conflicts, introduces a shared install_requirements_file helper in permission_utils.py that attempts a sudo wrapper install with fallback to user-level pip, and updates store_manager.py and api_v3.py to delegate to this helper.

Changes

Requirements Installation Consolidation

Layer / File(s) Summary
Pip install flag updates
first_time_install.sh
Adds --ignore-installed to pip install calls in the main requirements loop and web dependency install paths, updates manual recovery instructions, and adds explanatory comments.
Shared install helper
src/common/permission_utils.py
Adds install_requirements_file(req_file, timeout=300) that tries a sudo-wrapper install via safe_pip_install.sh, distinguishes sudo denial from pip failure, and falls back to user-level pip install --break-system-packages using sys.executable, always returning a CompletedProcess.
Consumer wiring
src/plugin_system/store_manager.py, web_interface/blueprints/api_v3.py
Replaces direct subprocess pip calls with calls to install_requirements_file, checking returncode/stderr instead of catching CalledProcessError, and removes the previously inlined sudo-wrapper/fallback logic in api_v3.py.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant StoreManager as PluginStoreManager
  participant ApiV3 as api_v3._pip_install_requirements
  participant Helper as install_requirements_file
  participant Wrapper as safe_pip_install.sh (sudo)
  participant Pip as pip (user fallback)

  StoreManager->>Helper: install_requirements_file(requirements_file, timeout=300)
  ApiV3->>Helper: install_requirements_file(req_file, timeout)
  Helper->>Wrapper: sudo -n bash safe_pip_install.sh req_file
  alt sudo denied or wrapper missing
    Helper->>Pip: sys.executable -m pip install --break-system-packages -r req_file
    Pip-->>Helper: CompletedProcess
  else wrapper handles install
    Wrapper-->>Helper: CompletedProcess
  end
  Helper-->>StoreManager: CompletedProcess (check returncode)
  Helper-->>ApiV3: CompletedProcess
Loading

Possibly related PRs

  • ChuckBuilds/LEDMatrix#355: Both PRs modify the plugin dependency installation flow in src/plugin_system/store_manager.py.
  • ChuckBuilds/LEDMatrix#371: Both PRs add pip install --ignore-installed to fix apt/pip conflicts in dependency-install commands.
  • ChuckBuilds/LEDMatrix#380: Both PRs route requirements installation through the scripts/fix_perms/safe_pip_install.sh sudo wrapper and update _pip_install_requirements.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main changes: fixing dependency installation in the Plugin Store and first-time install flow.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/plugin-dependency-auto-install-lknpd8

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codacy-production

codacy-production Bot commented Jul 8, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 11 complexity

Metric Results
Complexity 11

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

claude added 2 commits July 8, 2026 12:52
…quirements_file

Codacy's generic subprocess-security rule (Bandit B603 equivalent) flagged
the pip/sudo subprocess.run calls in install_requirements_file for lacking a
"static string argument" — the standard pattern-based flag for any
subprocess.run() call with a variable in its argv list. Both calls use
list-form argv (no shell=True, so no shell-injection surface), and the only
dynamic value is req_file, a Path built internally by callers rather than
raw external input; safe_pip_install.sh independently re-validates it before
installing anything as root. Suppresses with inline `# nosec B603` comments
matching this codebase's existing convention (see permission_utils.py's own
PROTECTED_SYSTEM_DIRECTORIES, display_manager.py, sync_manager.py, etc.).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X1NnDduw53kTe67i5zWwYx
web_interface/requirements.txt and requirements.txt both pin
requests>=2.33.0,<3.0.0, but Raspberry Pi OS ships an apt-managed
python3-requests with no pip RECORD file. Upgrading it via plain
`pip install` aborts with "uninstall-no-record-file" because pip refuses to
uninstall a package it has no record of, in place — which is exactly the
"Some web interface dependencies failed to install" warning first-time
install hits.

scripts/install_dependencies_apt.py and scripts/fix_perms/safe_pip_install.sh
already work around this with --ignore-installed (lets pip lay the new
version down in /usr/local, shadowing the apt copy, instead of trying to
remove it first). first_time_install.sh's own direct pip invocations —
the per-package requirements.txt loop, the web_interface/requirements.txt
install, and the requirements_web_v2.txt fallback — didn't have it. Adds
--ignore-installed to all three so first-time install no longer fails on
this well-known apt/pip conflict.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X1NnDduw53kTe67i5zWwYx
@ChuckBuilds ChuckBuilds changed the title fix: install plugin dependencies through root-visible installer in Plugin Store fix: dependency installation gaps in Plugin Store and first-time install Jul 8, 2026
The prior # nosec B603 comments suppressed Bandit's check but Codacy's
semgrep-based rule ("subprocess function 'run' without a static string")
kept flagging the same two lines as a critical security issue even after
that fix landed. install_dependencies_apt.py's _run() already needed both
tags together (# nosec B603 B607 ... # nosemgrep) for the identical
subprocess.run pattern, so apply the same double suppression here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X1NnDduw53kTe67i5zWwYx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/common/permission_utils.py (1)

383-386: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Fallback install should include --ignore-installed. The direct pip install path can still hit the apt-managed no RECORD file was found failure; keep it aligned with safe_pip_install.sh.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/common/permission_utils.py` around lines 383 - 386, The direct pip
fallback in permission_utils.py should be aligned with safe_pip_install.sh by
including --ignore-installed in the subprocess.run invocation used for the pip
install path. Update the argument list in the install helper that builds the
sys.executable -m pip command so it passes --ignore-installed along with
--break-system-packages and -r req_file, keeping the behavior consistent for
apt-managed packages that trigger the no RECORD file was found failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/common/permission_utils.py`:
- Around line 383-386: The direct pip fallback in permission_utils.py should be
aligned with safe_pip_install.sh by including --ignore-installed in the
subprocess.run invocation used for the pip install path. Update the argument
list in the install helper that builds the sys.executable -m pip command so it
passes --ignore-installed along with --break-system-packages and -r req_file,
keeping the behavior consistent for apt-managed packages that trigger the no
RECORD file was found failure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8ad434b9-48c1-42a6-b183-abdc52ec475f

📥 Commits

Reviewing files that changed from the base of the PR and between 9b2f026 and 6b0c7bd.

📒 Files selected for processing (4)
  • first_time_install.sh
  • src/common/permission_utils.py
  • src/plugin_system/store_manager.py
  • web_interface/blueprints/api_v3.py

CodeRabbit review caught this (confirming a gap already flagged in
conversation): the non-sudo fallback pip install in install_requirements_file
was missing --ignore-installed, unlike the sudo-wrapper branch and
safe_pip_install.sh. Without it, the same apt/pip RECORD-file conflict this
PR fixes elsewhere (first_time_install.sh, install_dependencies_apt.py) could
still hit installs that fall back to this path (e.g. a plugin's
requirements.txt on a host where safe_pip_install.sh isn't set up yet).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X1NnDduw53kTe67i5zWwYx
@ChuckBuilds ChuckBuilds merged commit 63a233f into main Jul 8, 2026
8 checks passed
@ChuckBuilds ChuckBuilds deleted the claude/plugin-dependency-auto-install-lknpd8 branch July 8, 2026 14:01
ChuckBuilds pushed a commit that referenced this pull request Jul 8, 2026
Same generic Bandit/semgrep pattern-match on non-literal subprocess.run
argv flagged in #385's install_requirements_file, now on the new
--ignore-installed retry call added here: list-form argv (no shell=True),
sys.executable is this process's own interpreter, and requirements_file is
built internally by find_plugin_directory, never raw external input.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X1NnDduw53kTe67i5zWwYx
ChuckBuilds added a commit that referenced this pull request Jul 8, 2026
…conflicts (#386)

* fix: plugin_loader retries with --ignore-installed before assuming apt package satisfies pin

install_dependencies treated any "uninstall-no-record-file" pip failure as
"dependency satisfied" and wrote the success marker without ever attempting
--ignore-installed, unlike install_dependencies_apt.py and
safe_pip_install.sh (added in #385 for the Plugin Store/first-time-install
paths). A plugin pinning a newer version of a system-managed package (e.g.
requests) would silently keep running against whatever version apt shipped,
while the marker file claimed the pinned requirement was met.

Now retries the same install with --ignore-installed on that specific
failure so pip actually lays the pinned version down (shadowing the
system-managed copy) before falling back to the prior tolerant behavior if
the retry itself fails too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X1NnDduw53kTe67i5zWwYx

* chore: suppress Codacy finding on new retry subprocess.run call

Same generic Bandit/semgrep pattern-match on non-literal subprocess.run
argv flagged in #385's install_requirements_file, now on the new
--ignore-installed retry call added here: list-form argv (no shell=True),
sys.executable is this process's own interpreter, and requirements_file is
built internally by find_plugin_directory, never raw external input.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X1NnDduw53kTe67i5zWwYx

* fix: tolerate a timed-out --ignore-installed retry, dedupe marker-write logic

CodeRabbit review caught a real inconsistency: if the --ignore-installed
retry itself timed out, subprocess.TimeoutExpired propagated to the outer
handler and returned False, failing plugin load — contradicting the
intended "tolerate this specific apt/pip conflict" behavior, where a mere
non-zero retry return code already returns True. Wraps the retry in its own
try/except so a timeout is logged and tolerated the same way as any other
retry failure.

Also extracts the marker-writing logic (open/write/chmod, ignoring OSError)
into _write_dependency_marker, since it was duplicated identically between
the direct-success path and the apt-conflict-retry-fallback path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X1NnDduw53kTe67i5zWwYx

* fix: revert marker-write dedup helper, resolves CodeQL path-injection alert

CodeQL flagged _write_dependency_marker's open(marker_file, ...) as
"uncontrolled data used in path expression" (high severity) once the
marker-write logic was extracted into its own method. marker_file is
actually safe — it's built from safe_plugin_dir, which install_dependencies
sanitizes via os.path.basename() (CodeQL's own recognized py/path-injection
sanitizer, per the existing comment a few lines above) — but CodeQL's
interprocedural analysis doesn't carry that sanitized status across the new
method boundary, since the sanitizer call and the open() sink were no
longer in the same function.

This exact code produced zero CodeQL findings before the extraction (in two
duplicated inline blocks) and is unchanged in what data reaches it — only
its location moved. Reverting the extraction (keeping CodeRabbit's other,
independent timeout-handling fix) restores the previously-clean shape rather
than trying to convince the analyzer's cross-function taint tracking that a
refactor changed nothing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X1NnDduw53kTe67i5zWwYx

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants